package fr.cephb.locustree;

import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringWriter;



import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.OperationStatus;

import com.sleepycat.je.Transaction;

import fr.cephb.berkeleydb.db.Walker;
import fr.cephb.io.IOUtils;
import fr.cephb.json.JSONBuilder;


/**
 * LTSearch
 * @author lindenb
 *
 */
public class LTSearch
	{
	private static final Logger LOG=Logger.getLogger("fr.cephb");
	private static final int DEFAULT_LIMIT=500;
	
	private LTModel model;
	private Transaction txn=null;
	private int limit=DEFAULT_LIMIT;
	private boolean case_sensible=false;
	private boolean whole_word=true;
	private boolean regular_expression=true;
	private long time_out=-1L;
	private boolean query_timed_out=false;
	/**
	 * LTSearch
	 */
	public LTSearch()
		{
		}
	
	
	public void setTransaction(Transaction txn)
		{
		this.txn=txn;
		}
	
	public void setModel(LTModel model) {
		this.model = model;
		}
	
	public LTModel getModel()
		{
		return model;
		}
	
	public Transaction getTransaction()
		{
		return this.txn;
		}
	
	public void setCaseSensible(boolean caseSensible) {
		case_sensible = caseSensible;
		}
	
	public boolean isCaseSensible() {
		return case_sensible;
	}
	
	public void setTimeout(long timeOut)
		{
		time_out = timeOut;
		}
	
	public long getTimeout()
		{
		return time_out;
		}
	
	public boolean isLastQueryTimedOut()
		{
		return query_timed_out;
		}
	
	public void setWholeWord(boolean wholeWord) {
		this.whole_word = wholeWord;
	}
	
	public boolean isWholeWord() {
		return whole_word;
	}
	
	public void setLimit(int limit) {
		this.limit = limit;
	}
	
	public int getLimit() {
		return limit;
	}
	
	public boolean isRegularExpression() {
		return regular_expression;
	}
	
	public void setRegularExpression(boolean b)
		{
		regular_expression = b;
		}
	
	
	public void search(String line) throws DatabaseException,PatternSyntaxException
		{
		long time_start=System.currentTimeMillis();
		this.query_timed_out=false;
		try
			{
			
			if(!isRegularExpression() &&
					isWholeWord() &&
					isCaseSensible())
				{
				searchWholeWordNotRegex(line,time_start);
				return;
				}
			
			
			if(!isRegularExpression())
				{
				line=Pattern.quote(line);
				}
			
			Pattern pat=Pattern.compile(line.trim(),
					isCaseSensible()?0:Pattern.CASE_INSENSITIVE
					);
			search(pat,time_start);
			}
		catch(PatternSyntaxException err)
			{
			throw err;
			}
		}
	
		private int searchInNode(LTNode node,int hit_count,Pattern regex)
			{
			if(node==null) return hit_count;
			if(!node.hasEntities()) return hit_count;
			//we know that this node contains the query , search the object in the entities
			for(MappedObject o:node.getEntities())
				{
				if(hit_count>= getLimit()) break;
				if( match(regex,o.getId()) ||
					match(regex,o.getName()) ||
					matchObject(regex,o.getJSON()))
					{
					report(o,node);
					hit_count++;
					}
				}
			return hit_count;
			}
	
		
		private void searchWholeWordNotRegex(String word,final long time_start) throws DatabaseException
			{
			//LOG.info("searching for whole word "+word);
			Pattern regex= Pattern.compile(Pattern.quote(word),isCaseSensible()?0:Pattern.CASE_INSENSITIVE);
			int hit_count=0;
			boolean first=true;
			Walker<String,Long> w=null;
			try
				{
				w=getModel().getKeyword2nodeId().openWalker(getTransaction());
				
				while((first?w.getSearchKey(word):w.getNextDup())==OperationStatus.SUCCESS)
					{
					first=false;
					if(hit_count>= getLimit()) break;
					if(breakTimeout(time_start)) break;
					LTNode node= getModel().findNodeById(getTransaction(), w.getValue());
					hit_count=searchInNode(node,hit_count,regex);
					}
				}
			catch (DatabaseException e)
				{
				throw e;
				}
			finally
				{
				if(w!=null) w.close();
				}
			}
	
	
		private void search(Pattern regex,long time_start) throws DatabaseException
			{
			//LOG.info("searching all nodes for "+regex.pattern());
			int hit_count=0;
			Walker<Long, LTNode> w=null;
			try
				{
				w=getModel().getLocusNodeDB().openWalker(getTransaction());
				while(w.next())
					{
					if(hit_count>= getLimit()) break;
					if(breakTimeout(time_start)) break;
					LTNode node= w.getValue();
					hit_count=searchInNode(node,hit_count,regex);
					}
				}
			catch (DatabaseException e)
				{
				throw e;
				}
			finally
				{
				if(w!=null) w.close();
				}
			}
		
		private boolean breakTimeout(final long time_start)
			{
			if(getTimeout()>0L && (System.currentTimeMillis()-time_start)>getTimeout())
				{
				this.query_timed_out=true;
				return true;
				}
			return false;
			}
		
		private void report(MappedObject o,LTNode root)
			throws DatabaseException
			{
			//search root for this node
			if(root==null)
				{
				return;
				}
			
			//find track/chromosome for this node
			
			TrackChrom tc=getModel().getTrackChromFromNodeId(getTransaction(), root.getId());
			if(tc==null)
				{
				LOG.warning("cannot find trackchrom for "+root.getId());
				return;
				}
			
			//find organism for this chromosome
			
			
			ChromInfo chrom= getModel().getChromDB().get(getTransaction(), tc.getChromosomeId());
			Track track= getModel().getTracksDB().get(getTransaction(), tc.getTrackId());
			if(chrom==null || track==null) 
				{
				LOG.info("cannot find track/chrom for "+tc);
				return;
				}
			
			Organism organism=getModel().getOrganismDB().get(getTransaction(), chrom.getOrganismId());
			if(organism==null)
				{
				LOG.info("cannot find organism for chr.ID. "+chrom);
				return;
				}
			
			found(root,organism, track,chrom,o);
			
			}
		
		
		protected void found(LTNode node,Organism organism,Track track,ChromInfo chrom,MappedObject o)
			{
			StringWriter w= new StringWriter();
			try
				{
				new JSONBuilder().write(w,o.getJSON());
				}
			catch (IOException e)
				{
				throw new RuntimeException(e);
				}
			System.out.println(
					organism.getName()+"\t"+
					track.getName()+"\t"+
					chrom.getName()+"\t"+
					o.getId()+"\t"+
					o.getName()+"\t"+
					o.getStart()+"\t"+
					o.getEnd()+"\t"+
					w.toString()
					);
			}
		
		
		private void read(BufferedReader in)
			throws IOException,DatabaseException
			{
			String line;
			while((line=in.readLine())!=null)
				{
				if(line.trim().length()==0) continue;
				search(line);
				}
			}
		
		private boolean match(Pattern regex,String s)
			{
			if(this.whole_word)
				{
				return (regex.matcher(s).matches());
				}
			else
				{
				return (regex.matcher(s).find());
				}
			}
		
		private boolean matchObject(Pattern regex,Object o)
			{
			if(o==null || (o instanceof Number)) return false;
			if(o instanceof Map<?,?>)
				{
				Map<?,?> m=Map.class.cast(o);
				for(Object k:m.keySet())
					{
					if(matchObject(regex,m.get(k))) return true;
					}
				}
			else if(o instanceof List<?>)
				{
				List<?> L=List.class.cast(o);
				for(Object i:L)
					{
					if(matchObject(regex,i)) return true;
					}
				}
			else if(o.getClass().isArray())
				{
				Object[] L=(Object[])(o);
				for(Object i:L)
					{
					if(matchObject(regex,i)) return true;
					}
				}
			else if(o instanceof String)
				{
				return match(regex,String.class.cast(o));
				}
			return false;
			}
	
	
	
		/**
		 * main
		 */
		public static void main(String[] args)
			{
			LTSearch app= new LTSearch();
			try
				{
				Set<String> plus= new HashSet<String>();
				String dbHome=null;
				
				int optind=0;
				while(optind< args.length)
					{
					if(args[optind].equals("-h") ||
					   args[optind].equals("-help") ||
					   args[optind].equals("--help"))
						{
						System.err.println("Options:");
						System.err.println(" -h help; This screen.");
						System.err.println(" -db-home <berkeley-db-home>");
						System.err.println(" -n <limit> default:"+app.limit);
						System.err.println(" -i :case-insensible>");
						System.err.println(" -t :not a whole word");
						System.err.println(" -q :not a regex/quote");
						System.err.println(" -e <string> add this query");
						return;
						}
					else if(args[optind].equals("-db-home"))
						{
						dbHome=args[++optind];
						}
					else if(args[optind].equals("-n"))
						{
						app.limit=Integer.parseInt(args[++optind]);
						}
					else if(args[optind].equals("-e"))
						{
						plus.add(args[++optind]);
						}
					else if(args[optind].equals("-i"))
						{
						app.case_sensible=true;
						}
					else if(args[optind].equals("-t"))
						{
						app.whole_word=false;
						}
					else if(args[optind].equals("-q"))
						{
						app.regular_expression=false;
						}
					else if(args[optind].equals("-log-level"))
						{
						LOG.setLevel(Level.parse(args[++optind]));
						}
					else if(args[optind].equals("--"))
						{
						optind++;
						break;
						}
					else if(args[optind].startsWith("-"))
						{
						System.err.println("Unknown option "+args[optind]);
						return;
						}
					else 
						{
						break;
						}
					++optind;
					}
				if(dbHome==null)
					{
					System.err.println("db-home not specified.");
					return;
					}
			
				
				app.model= new LTModel();
				app.model.open(new File(dbHome), false);
				
				if(optind==args.length)
                        {
                        if(plus.isEmpty())
                        	{
                        	app.read(new BufferedReader(new InputStreamReader(System.in)));
                        	}
                        else
                        	{
                        	for(String q:plus) app.search(q);
                        	}
                        }
                else
                        {
                        for(String q:plus) app.search(q);
                        while(optind< args.length)
                            {
                            
                          	java.io.BufferedReader r= IOUtils.openReader(args[optind++]);
                            app.read(r);
                            r.close();
                            }
                        }
				} 
		catch(Throwable err)
			{
			err.printStackTrace();
			}
		finally
			{
			if(app.model!=null) app.model.close();
			}
		}
	
	}
